
/***************************************************************************
 *   Copyright (C) 1997 to 2013 by Jonathan Duddington                     *
 *   email: jonsd@users.sourceforge.net                                    *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write see:                           *
 *               <http://www.gnu.org/licenses/>.                           *
 ***************************************************************************/

#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "dbox.h"
#include "event.h"
#include "os.h"
#include "werr.h"
#include "wimp.h"

#include "narc.h"
#include "hdrs.h"


extern void set_caret_pos(TEXTR *t);
extern void speak_titles(TEXTR *t);   /* in TEXT module */
extern int speak_load(void);          /* in OPTIONS module */

extern OPTIONS options;
extern BLIST boxlist;
extern VOICES voices;

int speak_command(TEXTR *t, int type);

TEXTR *speak_textr=NULL;
int  speak_start2= -1;    /* remember start of this speech */
int  clause_start= -1;    /* start of current speaking clause */
int  speak_type = 0;
int  scroll_as_you_speak=0;
int  pause_position=0;
int  grab_caret=0;           /* 1= give text window input focus or next highlight */
int  speak_pause[4] = {0,0,0,0};   /* x 10 mS, clause, sentence, parag. */

int  inhibit_speak_cursor = 0;

static int speak_key = 0;


int speak_loaded = 0;
int  speak_inhibit = 0;





void speak_set_params(int speed,int volume)
/*************************************/
{
	static short speed_factor[] = {128, 0,30,58,68,78,88,98,108,118,128, 137,146,155,164,173,182,191,200,209,218,227,236,245,255,265};

	static short volume_factor[] = {128, 0,15,30,45,59,73,87,101,115,128,138,148,159,170,182,196,210,224,238,255};

	if(speed > 0)
	{
		os_swi1(SWI_SPEAK+8,speed_factor[speed]);
		if(speed <= 2)
			os_swi1(SWI_SPEAK+9,1);  /* word_gap */
		else
			os_swi1(SWI_SPEAK+9,0);
	}

	if(volume > 0)
		os_swi1(SWI_SPEAK+13,volume_factor[volume]);
}   /* end of speak_set_params */



void announce_message(char *string, int level)
/****************************************/
{
	/* inform MoreDesk_FlashWindow */
	os_swi3(0x78742, 3, boxlist.w_buttons, 500);  // 5 second flash

	if(speak_inhibit)
		return;

	if(options.speak_announcements)
	{
		speak_load();

		if(level > 0)
		{
			// slow, except for message titles
			speak_set_params(options.speak_speed_announce, options.speak_volume);
		}
		os_swi3(SWI_SPEAK+14, (int)voices.voicename[0], 0, 0);
		os_swi6(SWI_SPEAK+3,(int)string, strlen(string), -1, '`',speak_key,0);
	}
}   /* end of announce_message */



void speak_string(char *string)
/*****************************/
/* speak string, but stop at '|' character */
{
	char *p;
	int  length=0;

	if(speak_inhibit)
		return;

	speak_load();

	if((p = strchr(string,'|'))!=NULL)
		length = p-string;

	speak_set_params(options.speak_speed_announce, options.speak_volume);
	os_swi3(SWI_SPEAK+14, (int)voices.voicename[1], 0, 0);
	os_swi6(SWI_SPEAK+3,(int)string, length, -1, 0,speak_key,0);
}   /* end of speak_string */





void speak_word(TEXTR *t, int index)
/**********************************/
/* Speak the word at the cursor position (Talk-as-you-type).
*/
{
	int  c;
	char *ptr;
	char *bufptr;
	char *bufend;
	char buf[90];

	if(speak_inhibit)
		return;

	buf[0] = 0;
	bufptr = buf;
	bufend = &buf[sizeof(buf)-1];

	if(speak_loaded == 0)
		speak_load();

	ptr = &t->text_base[index];

	if(!isalnum(*ptr) && (*ptr != '\''))
	{
		if(isalnum(ptr[-1]))
			ptr--;    /* cursor is just after a word, speak previous word */
		else
			return;   /* cursor not on a word */
	}

	/* find start of current word  \221 = sexed quote */
	while((ptr >= t->text_base) && (isalnum(c = *ptr) || (strchr("'.:!\221",c) != NULL)))
	{
		ptr--;
		if(strchr(".:!",c) != NULL)
			break;
	}

	ptr++;

	while((*ptr > ' ') && (bufptr < bufend) && (ptr < (t->text_base + t->text_body_end)))
	{
		*bufptr++ = *ptr++;
	}

	if((bufptr > &buf[1]) && (bufptr[-1] == '-') && (isalpha(bufptr[-2])))
		bufptr--;   /* ignore trailing hyphen */

	*bufptr++ = 0;

	/* use standard speak speed */
	speak_set_params(options.speak_speed, options.speak_volume);
	os_swi3(SWI_SPEAK+14, (int)voices.voicename[1], 0, 0);
	os_swi6(SWI_SPEAK+3,(int)buf, 0, -1, 0,speak_key,0);
}   /* end of speak_word */







void highlight_word(TEXTR *t, int ix)
/***********************************/
/* Highlight word being spoken */
{
	int  c;
	wimp_caretstr caretstr;

	/* move to start of next word */
	while((ix < (t->text_body_end-1)) &&
			(((c = t->text_base[ix]) =='"') || (strchr(" \n.,;:?')]}>",c)!=NULL)))
	{
		ix++;
	}

	t->cursor_index = ix;

	/* check whether the text window has the caret */
	wimp_get_caret_pos(&caretstr);
	if((caretstr.w == t->w_text) || (grab_caret))
	{
		grab_caret = 0;
		set_caret_pos(t);
	}

#ifdef deleted
	/* highlight word */
	region_start = cursor_index;
	p_end = p;
	while((p_end < ebuf_end) && ((isalnum(*p_end)) || (*p_end=='\'')))
		p_end++;
	region_end = p_end - ebuf_base;

	if((last_highlight_line >= 0) && (last_highlight_line != cursor_line))
		redraw_line(last_highlight_line);   /* remove highlight */
	redraw_line(cursor_line);
	last_highlight_line = cursor_line;
#endif

}   /* end of highlight_word */





void set_scroll_as_you_speak(TEXTR *t, int value)
/***********************************************/
/* Bit 1 indicates don't set icon state from here */
{
	int  flags;

	/* set the state of the button */
	flags = 0;
	if(value & 1)
	{
		flags = wimp_ISELECTED;
		scroll_as_you_speak = 1;
		grab_caret=1;    /* grab input focus for highlighting */
	}
	else
		scroll_as_you_speak = 0;

	if(!(value & 2))
		wimp_set_icon_state(t->w_card,t->lips_icon,flags,wimp_ISELECTED);


}   /* end of set_scroll_as_you_speak */




int find_clause(TEXTR *t, int ix)
/*******************************/
/* Find the end of the current clause.

   Also checks for blank line (paragraph) as end-of-clause indicator.

   Does not end clause for:
      punctuation immediately followed by alphanumeric  eg.  1.23  !Speak  :path
      repeated punctuation, eg.   ...   !!!
*/

{
	int  c;
	static char *match = ",.?!:;\217";   /* end-of-clause markers, including bullet point */
	char *p;
	char *p2;
	int  parag;
	char *text_end;
	int  current_quoting;

	/* is current line quoted ? */
	current_quoting = is_quoted_text(t->text_base + index_to_linestart(t,ix,NULL));

	p = t->text_base + ix;
	text_end = t->text_base + t->text_body_end;

	while(p < (text_end-1))
	{
		c = *(++p);

		if(c == '\n')
		{
			parag = 0;
			p2 = ++p;

			if(is_quoted_text(p2) != current_quoting)
			{
				p--;
				break;  /* change in quoting level */
			}

			while((p2 < text_end) && (*p2 <= ' '))
			{
				if(*p2 == '\n')
				{
					parag = 1;
					break;
				}
				p2++;
			}
			if(parag)
			{
				break;  /* end of paragraph, but no end-of-sentence */
			}
		}

		if(strchr(match,c) == NULL)
			continue;

		if((p[1] > ' ') && !isspace(p[1]))
			continue;      /* ignore ! ? .  unless followed by a blank */

		break;
	}
#ifdef deleted
	{	// DEBUG
		FILE *fv;
		fv=fopen("log_espeak","a");
		fprintf(fv,"FC %4d -> %4d (tb=%x) ",ix,p-t->text_base, t->text_base);
		for(ix=0; ix<10; ix++)
		{
			if((c = p[ix]) == '\n')
				c = '_';
			fputc(c,fv);
		}
		fputc('\n',fv);
		fclose(fv);
	}
#endif
	return(p - t->text_base);
}   /* end of find_clause */






int find_word_start(TEXTR *t, int ix)
/***********************************/
{
	/* find start of current word */
	while((t->text_base[ix] > ' ') && (ix > 0))
	{
		ix--;
	}
	return(ix);
}   /* end of find_word_start */




int find_clause_start(TEXTR *t, int ix)
/*************************************/
/* Find end of previous clause, or sentence */
{
	int  c;
	static char *match = ",.?!:;\217";
	int  end_ix;

	if(ix == 0)
		return(0);

	/* Ignore blanks and punctuation at end of text.
	   Find last alphanumeric in the text */
	end_ix = t->text_body_end - 1;

	while((end_ix > 0) && (!isalnum(t->text_base[end_ix])))
		end_ix--;

	if(ix > end_ix)
		ix = end_ix;

	while(ix >= t->text_start)
	{
		c = t->text_base[--ix];

		if(c <= ' ')
			break;    /* look for start of word */

		if((c == '\n') && (t->text_base[ix-1] == '\n'))
			break;

		if(strchr(match,c) == NULL)
			continue;

		if(!isalnum(t->text_base[ix+1]))
			break;      /* ignore ! ? .  followed by letter or digit */
	}
	return(ix+1);
}   /* end of find_clause_start */




void speak_stop(TEXTR *t)
/*********************/
{

	wimp_set_icon_state(t->w_card,t->lips_icon,0,wimp_ISELECTED);  /* un-select icon */

	if(t != speak_textr)
		return;

	if(t->speak_end > 0)
		os_swi1(SWI_SPEAK+5,0);

	t->speak_end = -1;        /* stop speaking */

	null_events(1,0);      /* don't want nulls for speech */

}   /* speak_stop */







void toggle_speak(TEXTR *t, int state)
/************************************/
/* state: 0 stop immediately, 1 on, 2 off */
{
	int  flags;

	speak_load();

	/* set the state of the button */
	flags = 0;
	if(state==0)
	{
		speak_stop(t);
	}
	if(state == 1)
	{
		flags = wimp_ISELECTED;
		if(speak_command(t,4) < 0)
		{
			flags = 0;
		}
		wimp_set_icon_state(t->w_card,t->lips_icon,flags,wimp_ISELECTED);
	}
	else
	{
		null_events(1,0);      /* don't want nulls for speech */
	}
}   /* end of toggle_speak */






#define CHAR_EMPHASISE  0x9d  /* double-dagger */






void speak_expand_text(char *input, int length, int voice)
/********************************************************/
/* insert beeps to mark words which start with or contain capital letters */
{
	int  c;
	char *p;
	char *out;
	int  nl_count;
	int  len;
	char *end;
	char *punct_list;
	char buf[1000];

	nl_count = 0;
	out = buf;
	p = input;
	end = &input[length];

	punct_list = NULL;
	if(options.speak_punct & 3)
		punct_list = "";
	os_swi3(SWI_SPEAK+14, (int)voices.voicename[voice], punct_list, 0);

	p = input;
	while(p < end)
	{
		/* start of a word */
		input = p;

		while((p < end) && (c = *p++) > ' ')
		{
			if((nl_count > 1) && options.speak_punct & 3)
			{
				/* speak 'paragraph' */
//            os_swi6(SWI_SPEAK+3,(int)"Paragraph\n\n", 0, -1,CHAR_EMPHASISE,speak_key,0);
			}
			nl_count = 0;
		}

		if(c == '\n')
			nl_count++;

		len = p - input;

		if((out-buf + len) >= (sizeof(buf)-60))
		{
			break;   /* maximum length */
		}


		memcpy(out,input,len);
		out += len;

	}

	if((nl_count > 1) && options.speak_punct & 3)
	{
		/* speak 'paragraph' */
		strcpy(out,"\n\nParagraph\n\n");
		out+=13;
	}

	buf[out-buf] = 0;
	os_swi6(SWI_SPEAK+3,(int)buf, out-buf,-1,CHAR_EMPHASISE,speak_key,0);

}   /* end of speak_expand_text */





void speak_text_continue()
/************************/
{
	int  ready;
	int  length;
	int  i;
	int  clause_end;
	int  j;
	int  source_ix;
	TEXTR *t;
	char *p;
	char *p_end;
	int  voice;
	int  quote_level;
	int  email_sign;
	int  email_end;
	int  emphasize;
	static int  clause_type=3;    /* 0=clause, 1=sentence, 2=paragraph, 3=none */
	static int do_speak=0;        /* 0=scroll, 1=speak */
	static int speak_pause_time = 0;   /* wait until this time */
	static int source_index = 0;
	char buf[512];

	if(speak_inhibit)
		return;

	t = speak_textr;
	if(t->text_base == NULL)
		return;

	os_swi2r(SWI_SPEAK+0,0,0,&ready,&source_ix);


	if(source_ix != source_index)
	{
		source_index = source_ix;
		/* move cursor to word being spoken */
		if((speak_start2 != -1) && (scroll_as_you_speak) && !inhibit_speak_cursor)
			highlight_word(t,source_ix + clause_start);
	}

	if(!ready) return;

	if((t->speak_start >= t->speak_end) || (t->speak_start >= t->text_body_end))
	{
		if((t->text_type == X_VIEW) &&
				((t->speak_end >= t->sig_start) || (t->speak_end >= t->text_body_end)))
		{
			if(options.speak_next)
			{
				if(card_open_next(t->fr,5,1,0)==0)
				{
					wimp_set_icon_state(t->w_card,t->lips_icon,wimp_ISELECTED,wimp_ISELECTED);
					speak_command(t,5);
					return;
				}
			}
			if(options.speak_next_unread)
			{
				if(card_open_next(t->fr,7,1,0)==0)
				{
					wimp_set_icon_state(t->w_card,t->lips_icon,wimp_ISELECTED,wimp_ISELECTED);
					speak_command(t,5);
					return;
				}
			}
		}

		toggle_speak(t,2);

		if((scroll_as_you_speak) && (t->speak_end != -1))
		{
			t->cursor_index = t->speak_end;
			set_caret_pos(t);
		}

		if(t->speak_end >= t->text_body_end)
		{
			/* reached end of text, turn off speech-scroll */
			set_scroll_as_you_speak(t,0);
		}


		t->speak_end = -1;
		wimp_set_icon_state(t->w_card,t->lips_icon,0,wimp_ISELECTED);  /* un-select icon */
		return;
	}

	if(!do_speak)
	{
		/* now wait for next null event */
		do_speak = 1;
		if(speak_pause[clause_type] > 0)
		{
			os_swi1r(0x42,0,&speak_pause_time);   /* OS_ReadMonotonicTime */
			speak_pause_time += speak_pause[clause_type];
		}
		else
		{
			speak_pause_time = 0;
		}
	}
	else
	{
		if(speak_pause_time != 0)
		{
			/* waiting for a delay */
			os_swi1r(0x42,0,&i);       /* OS_ReadMonotonicTime */
			if(i < speak_pause_time)
				return;
		}

		/* continue speaking text */
		p = t->text_base + t->speak_start;
		p_end = t->text_base + t->text_body_end;
		while((p < p_end) && !isalnum(*p)) p++;

		if((p[0]=='I') && (p[1]=='n') && (p[2]==' '))
		{
			if(((memcmp(p+4,"rticle",6)==0) || (memcmp(p+4,"essage",6)==0)) &&
					((p[11]=='<') || (p[12]=='<')))
			{
				/* skip over In-article */
				while((*p != '\n') && (*p != ','))
					p++;
				t->speak_start = p+1 - t->text_base;
			}
		}




		clause_end = find_clause(t,t->speak_start)+1;
		if(clause_end > t->speak_end)
			clause_end = t->speak_end;

		length = clause_end-t->speak_start;

		if(length > 0)
		{
			/* does this line start with quotation indicator ? */
			p = &t->text_base[t->speak_start];
			while((p < &t->text_base[t->text_body_end]) && !isalnum(*p))
				p++;
			while((p > t->text_base) && (*p != '\n'))
				p--;

			if(*p == '\n') p++;

			quote_level = is_quoted_text(p);
			if(quote_level > 3)
				quote_level = 3;
			voice = quote_level+2;

			p = &t->text_base[t->speak_start];

			if(length < sizeof(buf))
			{
				/* remove any email addresses */
				memcpy(buf,p,length);

				for(i=0; i<length; i++)
				{
					if((p[i] == '*') || (p[i] == '_'))
					{
						/* look for *word* and replace * by emphasis */
						emphasize = p[i];
						for(j=i+1; j<length; j++)
						{
							if(p[j] == emphasize)
							{
								break;
							}

							if(!isalpha(p[j]) && (p[j] != '\''))
							{
								emphasize = 0;
								i = j;
								break;
							}
						}
						if(emphasize)
						{
							buf[i] = '"';  /* short pause for emphasis */
							buf[i+1] = CHAR_EMPHASISE;
							memcpy(&buf[i+2],&p[i+1],j-i-1);
							i = j+1;
						}
					}

					if((p[i] == '<') && (options.speak_skip_addresses))
					{
						email_sign = 0;
						email_end = 0;
						for(j=i+1; j<length; j++)
						{
							if(p[j] <= ' ')
							{
								email_sign = 0;
								break;

							}

							if((p[j] == '@') && isalnum(p[j-1]) && isalnum(p[j+1]))
								email_sign = j;

							if(p[j] == '>')
							{
								email_end = j;
								break;
							}
						}
						if((email_sign != 0) && (email_end > i))
						{
							while(i <= email_end)
							{
								buf[i++] = ' ';
							}
						}
					}
				}
				p = buf;
			}

			speak_set_params(options.speak_speed,options.speak_volume);

			speak_expand_text(p,length,voice);

			clause_start = t->speak_start;
			inhibit_speak_cursor = 0;
		}

		t->speak_start = clause_end;   /* start of next clause to speak */
		do_speak = 0;   /* scroll first */

	}

}   /* end of speak_text_continue */






void speak_text(int start, int end)
/*********************************/
{
	TEXTR *t;

	end++;     /* character after the end */

	t = speak_textr;

	t->speak_start = start;
	t->speak_end = end;

	while((t->speak_start < t->speak_end) && (t->text_base[t->speak_start] <= ' '))
		t->speak_start++;

	speak_start2 = t->speak_start;
	clause_start = t->speak_start;

	null_events(1,1);   /* allow for speech */
}   /* end of speak_text */









int speak_command(TEXTR *t, int type)
/********************************/
{
	int  ix;
	int  end_ix;
	os_error *os_err;

	if(speak_inhibit)
		return(-1);

	/* check that speak module is present */
	os_err = os_swi0(SWI_SPEAK+0);
	if(os_err)
	{
		werr(FALSE,"Speak module is not loaded");
		return(-1);
	}

	speak_type = type;
	speak_textr = t;

	switch(type)
	{
	default:
		inhibit_speak_cursor = 0;
		if(t->region_start < t->region_end)
		{
			/* there is a region, this that */
			speak_text(t->region_start,t->region_end);
		}
		else
		{
			end_ix = t->text_body_end-1;

			if((t->cursor_index <= t->text_start) && (options.speak_title || (type==5)))
			{
				speak_titles(t);
				inhibit_speak_cursor=1;
			}

			/* stop speaking at sig separator if we are starting before it */
			if((t->sig_start < t->text_length) && (t->cursor_index < t->sig_start))
			{
				end_ix = t->sig_start;
			}

			if(t->cursor_index < t->text_start)
				t->cursor_index = t->text_start;  /* was in hidden internet header */

			ix = find_clause_start(t,t->cursor_index);

			speak_text(ix,end_ix);
		}
		break;
	}

	set_scroll_as_you_speak(t,3);

	return(0);
}   /* end of speak_command */
